﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Data;
using System.Data.Linq;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Linq.Dynamic;
using System.Reflection;
using System.Text;
using System.Xml;
using IntraVision.Web.Filter;

namespace IntraVision.Web
{
    /// <summary>
    /// Контроллер для таблицы сущностей. Поддерживает поиск, фильтрацию, сортировку, постраничное листание.
    /// </summary>
    /// <typeparam name="TDataContext">DataContext - берется от модели</typeparam>
    /// <typeparam name="TEntity">Тип сущностей, с которыми работает контроллер.</typeparam>
    public abstract class TableController<TDataContext, TEntity> : EntityListController<TEntity,TDataContext>
        where TDataContext : DataContext, new()
        where TEntity : class, new()
    {
        protected string _TablePartialTemplatePath = "~/views/_table/table.haml";
        protected string _FilterTemplatePath = "~/views/_table/filter.haml";
        protected string _ColumnsTemplatePath = "~/views/_table/columns.haml";
        protected string _SavedStatesTemplatePath = "~/views/_table/savedstates.haml";
        
        /// <summary>
        /// Список колонок таблицы. Инициализируется вручную или с помощью XML конфигуратора
        /// </summary>
        public List<Column> Columns { get; set; }
        /// <summary>
        /// Список условий фильтров. Инициализируется вручную в методе CreateFilterConditions
        /// </summary>
        public Filter<TEntity> Filter { get; set; }
        //public List<IFilterCondition> Conditions { get; set; }

        /// <summary>
        /// Список сохраненных фильтров для страницы. Загружается автоматически.
        /// </summary>
        public List<SavedState> SavedStates {get; set; }
        /// <summary>
        /// Текущее состояние таблицы. Загружается из сессии, обновляется значениями из Request. Может быть сохранено и затем использовано.
        /// </summary>
        public NameValueCollection State { get; set; }
        /// <summary>
        /// Свойство для быстрого получения списка видимых колонок. Использовать только после вызова InitColumns()
        /// </summary>
        public Column[] VisibleColumns
        {
            get { return (from c in Columns where c.Visible == true orderby c.Order select c).ToArray(); }
        }
        /// <summary>
        /// Системные колонки
        /// </summary>
        public Column[] SystemColumns
        {
            get { return (from c in Columns where c.IsSystem == true select c).ToArray(); }
        }

        #region Служебные поля
        protected int _Count = 0;
        protected int _Page = 0;
        protected int _PageCount = 1;
        /// <summary>
        /// Количество записей на странице. Определяется состоянием таблицы.
        /// </summary>
        protected int _PageSize = 50;
        /// <summary>
        /// Путь к конфигуратору. Должен быть определен в конструкторе, если для страницы есть XML конфигуратор.
        /// </summary>
        protected string _ConfiguratorPath = null;
        protected XmlDocument _Configurator = null;

        /// <summary>
        /// Показывает, поддерживает ли таблица экспорт в эксель. Может быть задано через конфигуратор.
        /// </summary>
        protected bool _HasExcel = true;

        /// <summary>
        /// Показывает, поддерживает ли таблица сохраненные представления.  Может быть задано через конфигуратор.
        /// </summary>
        protected bool _SaveFilter = false;

        /// <summary>
        /// Имя колонки-первичного ключа. Если задано это свойство, то автоматически появится 
        /// колонка с чекбоксами и будет возможность удалять записи. Может быть задано через конфигуратор.
        /// </summary>
        protected string _PrimaryKey { get; set; }

        /// <summary>
        /// Если задано это свойство, в результирующую таблицу будет добавлена колонка "RowClass",
        /// содержащая в себе css класс текущего ряда таблицы. В дальнейшем в .haml эта колонка должна быть обработана
        /// </summary>
        protected bool _HasRowClass = false;

        /// <summary>
        /// Ключи, перечисленные в этом списке не будут сохранены в БД при сохранении состояния.
        /// Например, текущая страница влияет на состояние таблицы, но при сохранении фильтра ее не нужно сохранять.
        /// </summary>
        protected List<string> _ExcludeStateKeys = new List<string> { "tb_page" };

        /// <summary>
        /// Ключи, перечисленные в этом списке, не будут сброшены при сбросе фильтра и при загрузке фильтра из базы с помощью функции LoadState.
        /// Например, этот список может использоваться для задания дополнительных параметров фильтрации, которые важнее фильтра. Например, поле "Сервис" в Интрасервисе.
        /// </summary>
        protected List<string> _FilterIndependentKeys = new List<string>();

        /// <summary>
        /// Ключ, по которому получать состояние таблицы. Action конкретной таблицы может переопределить этот ключ.
        /// Например, если у UserController есть два метода - List и Select, эти методы могут определить разные значения этого ключа.
        /// </summary>
        protected string _ActionKey
        {
            get { return Session[Name + "_ActionKey"] != null ? Convert.ToString(Session[Name + "_ActionKey"], CultureInfo.InvariantCulture) : Action; }
            set { Session[Name + "_ActionKey"] = value; }
        }
        protected NameValueCollection _DefaultState = new NameValueCollection();
        #endregion

        public TableController()
        {
            Columns = new List<Column>();
            SavedStates = new List<SavedState>();
            State = null;
        }

        /// <summary>
        /// По умолчанию вызываем List
        /// </summary>
        public virtual void Default()
        {
            List();
        }

        /// <summary>
        /// Стандартный метод для вывода списка сущностей. 
        /// </summary>
        public override void List()
        {
            //Определим файл шаблона и лэйаут
            if (string.IsNullOrEmpty(TemplatePath))
            {
                if (!string.IsNullOrEmpty(Request["nolayout"])) TemplatePath = _TablePartialTemplatePath;
                else TemplatePath = "~/views/" + Name + "/list.haml";
            }
            if (string.IsNullOrEmpty(LayoutPath) && string.IsNullOrEmpty(Request["nolayout"])) LayoutPath = "~/views/layouts/page.haml";

            //Зарегистрировать стандартные скрипты для таблицы
            RegisterTableScripts();

            //Изменим ключ текущего состояния на _list
            _ActionKey = "list";
            //Изменим действие на "list", чтобы нормально обработать ситуацию, когда идет удаление.
            ViewData["Action"] = "list";

            //Вызовем стандартный жизненный цикл таблицы
            ProcessTable();
        }

        /// <summary>
        /// Выгрузить таблицу в Excel
        /// </summary>
        public virtual void Excel()
        {
            TemplatePath = "~/views/_table/excel.haml";
            LayoutPath = "~/views/layouts/excelpage.haml";

            Response.ContentType = "application/vnd.ms-excel";
            Response.AppendHeader("Content-Disposition", "attachment;filename=export.xls");

            ViewData["excel"] = true;
            List();
        }

        /// <summary>
        /// Для корректной работы таблицы ее методы должны быть вызваны в определенном порядке. Этот порядок определен в методе ProcessTable.
        /// Все методы, вызываемые в ProcessTable могут быть переопределены для изменения поведения таблицы.
        /// </summary>
        protected virtual void ProcessTable()
        {
            using (var dc = new TDataContext())
            {
                //Инициализация конфигуратора
                InitConfigurator();
                //Инициализация состояния таблицы
                InitState();
                //Кастомная инициализация
                CustomInit(dc);

                //Создание колонок и условий фильтра
                CreateColumns(dc);
                CreateFilterConditions(dc);

                //Инициализация колонок, фильтра и постраничного листания из загруженного состояния:
                // - Определение видимости, сортировки и порядка следования колонок 
                InitColumns();
                // - Состояние фильтра - значения полей-условий фильтра
                InitFilter();
                // - Размер страницы, текущая страница
                InitPaging();

                //Автоматическая генерация запроса к нужной таблице.
                var query = GenerateQuery(dc);

                try
                {
                    //Дополнительная фильтрация записей - проверка безопасности, особые условия фильтра и т.п.
                    //По умолчанию ничего не делает
                    query = CustomFilter(dc, query);
                    //Применение стандартных условий фильтра
                    query = FilterRows(dc, query);
                    //Поиск по подстроке
                    query = Search(query);
                    //Сортировка записей
                    query = Order(dc, query);
                    //Рассчет количества страниц, разбиение на страницы
                    query = Paginate(query);
                }
                catch (ParseException ex) //Неправильный фильтр или другая ошибка при обработке состояния
                {
                    ViewData.Exceptions.Add(ex);
                    //Нужно сбросить ошибочное состояние и использовать состояние по умолчанию.
                    Session[Name + _ActionKey] = _DefaultState;
                    //И не выбирать никакие записи, чтобы случайно не вывести те записи, на которые у человека нет прав,
                    //ведь ошибка может возникнуть и в методе "CustomFilter"
                    query = query.Where(q => false);
                }

                //Если тип исходных данных отличается от типа данных результата, должен быть переопределен метод ConvertQueryToResult
                //По умолчанию list будет равен query
                IQueryable list = query;
                list = ConvertQueryToResult(dc, query);

                //Превратим LINQ запрос в таблицу со строками в каждой ячейке.
                var dt = CreateDataTable(dc, list);

                ViewData["Entities"] = query;
                ViewData["DataTable"] = dt;

                RenderView();

                if (dc.Connection.State != ConnectionState.Closed) dc.Connection.Close();
            }
        }

        #region 1. Создание колонок и фильтров. Таблица должна перегрузить эти методы
        /// <summary>
        /// Инициализация конфигуратора.
        /// </summary>
        protected virtual void InitConfigurator()
        {
            if (string.IsNullOrEmpty(_ConfiguratorPath))
                _ConfiguratorPath = string.Format(CultureInfo.InvariantCulture, "~/xml/{0}.xml", Name);
            _Configurator = new XmlDocument();

            //Если файл найден - загрузим конфигуратор
            if (System.IO.File.Exists(Server.MapPath(_ConfiguratorPath)))
                _Configurator.Load(Server.MapPath(_ConfiguratorPath));
            else
                return;

            var tn = _Configurator.SelectSingleNode("/configurator/table[@action='" + _ActionKey + "']") as XmlElement;
            if (tn == null) tn = _Configurator.SelectSingleNode("/configurator/table") as XmlElement;
            if (tn == null) return;

            _PrimaryKey = tn.GetAttribute("primary-key");
            _HasExcel = !tn.HasAttribute("disable-excel");
            _HasRowClass = tn.HasAttribute("has-row-class");

            if (!_SaveFilter) _SaveFilter = !string.IsNullOrEmpty(tn.GetAttribute("save-filter")) && tn.GetAttribute("save-filter").ToUpperInvariant() == "TRUE";
        }

        /// <summary>
        /// Сформировать список колонок для таблицы. По умолчанию пытаемся загрузить из XML конфигуратора.
        /// </summary>
        protected virtual void CreateColumns(TDataContext dc)
        {
            CreateColumnsFromXml();
        }

        /// <summary>
        /// Сформировать список условий фильтра для таблицы.
        /// </summary>
        protected virtual void CreateFilterConditions(TDataContext dc) { }

        /// <summary>
        /// Загрузить колонки из XML-конфигуратора.
        /// </summary>
        protected virtual void CreateColumnsFromXml()
        {
            //TODO: Если не задан конфигуратор валится исключение ObjectReference - это неправильно
            if (_Configurator == null) return;
            try
            {
                var tn = _Configurator.SelectSingleNode("/configurator/table[@action='" + _ActionKey + "']");
                if (tn == null) tn = _Configurator.SelectSingleNode("/configurator/table");
                foreach (XmlElement el in tn.SelectNodes("column"))
                {
                    var col = new Column();
                    col.InitFromXml(el);
                    Columns.Add(col);
                }
            }
            catch (Exception ex)
            {
                ViewData.Exceptions.Add(ex);
            }
        }

        #endregion

        #region 2. Инициализация таблицы
        /// <summary>
        /// Инициализировать состояние по умолчанию.
        /// </summary>
        /// <returns>NameValueCollection</returns>
        protected virtual void InitDefaultState()
        {
            if (_Configurator != null)
            {
                var sn = _Configurator.SelectSingleNode("/configurator/table[@action='" + _ActionKey + "']/default-state");
                if (sn == null) sn = _Configurator.SelectSingleNode("/configurator/table/default-state");
                if (sn == null) return;

                _DefaultState.Clear();
                foreach (XmlElement xe in sn.SelectNodes("add"))
                    _DefaultState[xe.GetAttribute("key")] = xe.InnerText;
            }
        }

        /// <summary>
        /// Инициализировать текущее состояние таблицы.
        /// </summary>
        protected virtual void InitState() 
        {
            //Инициализация представления по умолчанию из конфигуратора (если он задан)
            //чтобы определить, применен ли фильтр и изменено ли состояние колонок.
            InitDefaultState();

            //StateKey - код состояния, который нужно считывать из сессии. У разных Action таблицы (например, Select и List) могут быть разные состояния
            string stateKey = Name + _ActionKey;
            ViewData["ActionKey"] = _ActionKey;

            //Сбросить фильтр
            if (!String.IsNullOrEmpty(Request["reset"]))
            {
                //Сохраним старое состояние
                var oldstate = (Session[stateKey] as NameValueCollection) != null ? new NameValueCollection(Session[stateKey] as NameValueCollection) : new NameValueCollection();
                //Перезапишем состояние в сессии
                State = new NameValueCollection(_DefaultState);
                //Восстановим те ключи, которые не должны быть сброшены
                foreach (var key in _FilterIndependentKeys)
                    State[key] = oldstate[key];

                Session[stateKey] = new NameValueCollection(State); //Перезапишем состояние в сессии
            }

            if (!string.IsNullOrEmpty(Request["stateid"])) //Передан идентификатор представления
            {
                int stateid;
                Int32.TryParse(Request["stateid"], out stateid);
                //Если номер представления задан правильно, загрузим его.
                if (stateid > 0) LoadState(stateid);
                //Если неправильно, используем представление по умолчанию.
                else State = _DefaultState;
            }
            else//Если не выбрано представление
            {
                //Загрузим состояние таблицы для нашего действия из сессии.
                State = Session[stateKey] as NameValueCollection;
                if (State == null && _SaveFilter) //Состояние не найдено, загрузим представление по умолчанию
                    LoadState(0);
                if (State == null) //Представление по умолчанию не найдено, возьмем чистую таблицу
                    State = new NameValueCollection(_DefaultState);

                //Обновим состояние таблицы значениями из Request
                foreach (string key in (from k in Request.Params.AllKeys where k!=null && (k.StartsWith("tb_") || k.StartsWith("flt") || k.StartsWith("ftt")) select k))
                    State[key] = Request[key];
            }

            //Если разрешается сохранять представления, загрузим список доступных представлений
            if (_SaveFilter)
            {
                InitSavedStates();
                ViewData["StateId"] = State["tb_stateid"];
                ViewData["AllowSavedStates"] = _SaveFilter;
            }

            //Если разрешается экспортировать список в Excel
            if (_HasExcel) ViewData["HasExcel"] = true;

            //Если был произведен поиск, запишем поисковое слово во ViewData
            ViewData["Search"] = State["tb_search"];
            if (!string.IsNullOrEmpty(_PrimaryKey)) ViewData["PrimaryKey"] = _PrimaryKey;

            //Запишем получившееся состояние в сессию
            Session[stateKey] = State;
            
            //Проверим, применен ли фильтр...
            if (FilterApplied(State)) ViewData["FilterApplied"] = true;
            //...и изменился ли список колонок. Если да, то подсветим соответствующие кнопки в интерфейсе.
            if(ColumnsChanged(State)) ViewData["ColumnsChanged"] = true;
        }

        /// <summary>
        /// Инициализируем колонки по ключам tb_columns и tb_orderby в состоянии таблицы
        /// </summary>
        protected virtual void InitColumns()
        {
            //Если колонки не были созданы или список видимых колонок не передан то вернуться
            if (Columns.Count == 0) return;

            //Определяем порядок вывода и видимость колонок
            if (!string.IsNullOrEmpty(State["tb_columns"]))
            {
                //Скрыть все колонки
                foreach (var c in Columns) c.Visible = false;
                //Показать выбранные
                string[] vcs = State["tb_columns"].Replace(" ","").ToLower().Split(',');
                int order = 1;
                foreach (string vc in vcs)
                {
                    var col = Columns.SingleOrDefault(c => c.Id.ToLower() == vc);
                    if (col != null && !col.IsSystem)
                    {
                        col.Order = order++;
                        col.Visible = true;
                    }
                }
            }

            //Определяем порядок сортировки
            if (!String.IsNullOrEmpty(State["tb_orderby"]))
            {
                //Получаем массив имен колонок, по которым нужно сортировать вида "Name asc","Id desc"
                string[] ocs = State["tb_orderby"].ToUpperInvariant().Split(',');
                int order = 1;
                foreach (string oc in ocs)
                {
                    if (string.IsNullOrEmpty(oc)) continue;
                    bool desc = oc.EndsWith("DESC");
                    string cn = oc;
                    if (oc.EndsWith(" DESC")) cn = oc.Substring(0, oc.Length - 5);
                    if (oc.EndsWith(" ASC")) cn = oc.Substring(0, oc.Length - 4);

                    var col = Columns.SingleOrDefault(c => c.SortAlias != null && c.SortAlias.ToUpperInvariant() == cn);
                    if (col != null)
                    {
                        col.SortOrder = order++;
                        col.SortDirection = desc ? SortDirection.Descending : SortDirection.Ascending;
                    }
                }
            }

            ColumnsInitialized();

            //После того, как колонки записаны во ViewData изменять коллекции колонок 
            //не имеет смысла - они все равно не попадут во ViewData
            ViewData["Columns"] = Columns;
            ViewData["VisibleColumns"] = VisibleColumns;
        }

        /// <summary>
        /// Дочерняя колонка может перегрузить эту функцию чтобы изменить колонки перед записью их во ViewData
        /// </summary>
        protected virtual void ColumnsInitialized() { }

        /// <summary>
        /// Инициализация фильтра
        /// </summary>
        protected virtual void InitFilter()
        {
            if (Filter != null)
            {
                Filter.Init(State);
                ViewData["Filter"] = Filter;
            }
        }

        /// <summary>
        /// Загрузка сохраненных представлений для данной страницы
        /// </summary>
        protected virtual void InitSavedStates()
        {
            try
            {
                SavedStates.Clear();
                using (var dc = new TDataContext())
                {
                    if (dc.Connection.State != ConnectionState.Open)
                        dc.Connection.Open();

                    IUser user = null;
                    if (ViewData.ContainsKey("user") && ViewData["user"] != null) user = ViewData["user"] as IUser;

                    var cmd = new SqlCommand("SELECT Id,Name,UserId,IsDefault FROM Filter WHERE Page = @Page AND (UserId IS NULL OR UserId = @UserId) ORDER BY UserId DESC, Name", (SqlConnection)dc.Connection);
                    cmd.Parameters.AddWithValue("@Page", Name);
                    cmd.Parameters.AddWithValue("@UserId", (user == null) ? 0 : user.Id);

                    using (SqlDataReader dr = cmd.ExecuteReader())
                    {
                        while (dr.Read())
                            SavedStates.Add(new SavedState { Id = dr.GetInt32(0), Name = dr.GetString(1), UserId = (dr[2] != DBNull.Value) ? (int?)dr.GetInt32(2) : null, Public = (dr[2] == DBNull.Value), Default = dr.GetBoolean(3) });
                    }

                    ViewData["SavedStates"] = SavedStates;
                }
            }
            catch (SqlException ex)
            {
                ViewData.Exceptions.Add(ex);
            }
        }

        /// <summary>
        /// Инициализация страниц
        /// </summary>
        protected virtual void InitPaging()
        {
            if(!string.IsNullOrEmpty(State["tb_page"])) Int32.TryParse(State["tb_page"], out _Page);
            if(!string.IsNullOrEmpty(State["tb_pagesize"])) Int32.TryParse(State["tb_pagesize"], out _PageSize);

            ViewData["PageSize"] = _PageSize;
        }

        /// <summary>
        /// Дополнительная инициализация дочерней таблицы
        /// </summary>
        /// <param name="dc"></param>
        protected virtual void CustomInit(TDataContext dc) { }
        #endregion

        #region 3. Работа с данными - поиск, фильтрация, сортировка
        /// <summary>
        /// Сформировать базовый запрос, на который в дальнейшем будут накладываться условия фильтра. По умолчанию возвращает dc.GetTable&lt;TEntity&gt;.
        /// </summary>
        /// <param name="dc">DataContext</param>
        /// <returns>Query</returns>
        protected virtual IQueryable<TEntity> GenerateQuery(TDataContext dc)
        {
            return dc.GetTable<TEntity>();
        }

        /// <summary>
        /// Стандартный фильтр. Применение стандартных условий фильтра, определенных для таблицы.
        /// </summary>
        /// <param name="dc"></param>
        /// <param name="query"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> FilterRows(TDataContext dc, IQueryable<TEntity> query)
        {
            if( Filter != null )
                query = Filter.ApplyFilter(query);
            return query;
        }

        /// <summary>
        /// Выполнить поиск.
        /// </summary>
        /// <param name="query">Query</param>
        /// <returns>Query</returns>
        protected virtual IQueryable<TEntity> Search(IQueryable<TEntity> query)
        {
            if (!string.IsNullOrEmpty(State["tb_search"])) query = query.Where(LINQHelper.GenerateSearchString(typeof(TEntity)), State["tb_search"]);
            return query;
        }

        /// <summary>
        /// Отсортировать список.
        /// </summary>
        /// <param name="dc"></param>
        /// <param name="query"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> Order(TDataContext dc, IQueryable<TEntity> query)
        {
            return (!string.IsNullOrEmpty(State["tb_orderby"])) ? query.OrderBy(State["tb_orderby"]) : query;
        }

        /// <summary>
        /// Отсортировать список.
        /// </summary>
        /// <param name="query">Query</param>
        /// <returns>Query</returns>
        protected virtual IQueryable<TEntity> Order(IQueryable<TEntity> query)
        {
            return Order(null, query);
        }

        /// <summary>
        /// Разбиение на страницы
        /// </summary>
        /// <param name="query">Query</param>
        /// <returns>Query</returns>
        protected virtual IQueryable<TEntity> Paginate(IQueryable<TEntity> query)
        {
            _Count = query.Count();
            if(!ViewData.ContainsKey("excel"))
            {
                if (_Count > _PageSize) _PageCount = (int)Math.Ceiling((double)_Count / _PageSize);
                if (_Page > _PageCount || _Page <= 0) _Page = 1;
            }
            else
            {
                _Page = 1;
                _PageCount = 1;
                _PageSize = 65500;
            }

            ViewData["Page"] = _Page;
            ViewData["PageCount"] = _PageCount;
            ViewData["Count"] = _Count;
            ViewData["PageSize"] = _PageSize;

            if (_PageSize > 0) query = query.Skip((_Page - 1) * _PageSize).Take(_PageSize);
            ViewData["CountOnPage"] = query.Count();
            return query;
        }

        /// <summary>
        /// Превращает запрос в выходные данные. Должен быть переопределен в конкретных таблицах.
        /// </summary>
        /// <param name="query">Query</param>
        /// <returns>IQueryable</returns>
        protected virtual IQueryable ConvertQueryToResult(TDataContext dc,IQueryable<TEntity> query)
        {
            return query;
        }

        /// <summary>
        /// Создать таблицу строк из списка объектов
        /// </summary>
        /// <param name="query">Query</param>
        /// <returns></returns>
        protected virtual DataTable CreateDataTable(TDataContext dc, IQueryable query)
        {
            DataTable dt = new DataTable();

            //Добавить видимые колонки в список колонок
            foreach (var c in VisibleColumns)
                dt.Columns.Add(c.Id);

            //Добавить системные колонки
            foreach (var c in SystemColumns)
                dt.Columns.Add(c.Id);

            //Добавить первичный ключ в список колонок, если его еще там нет
            if (!string.IsNullOrEmpty(_PrimaryKey) && !VisibleColumns.Any(vc => vc.Id == _PrimaryKey))
                dt.Columns.Add(_PrimaryKey);
            
            //Добавим колонку RowClass, если задано свойство HasRowClass
            if(_HasRowClass)
                dt.Columns.Add("RowClass");

            foreach (var entity in query)
            {
                var dr = dt.NewRow();
                var format = FormatRow(entity);
                for (int i = 0; i < format.Count; i++)
                    dr[i] = LINQHelper.ParseFormat(format[i], entity);
                dt.Rows.Add(dr);
                RowAdded(dc, entity, ref dr);
            }

            //Подсчет итогов
            CalculateTotal(query, dt);
            //Подготовить таблицу (выполнить дополнительные операции - дочерние контроллеры могут переопределить этот метод)
            PrepareDataTable(dc, ref dt);
            return dt;
        }

        /// <summary>
        /// Подсчет итогов для тех колонок, для которых это необходимо.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="dt"></param>
        protected virtual void CalculateTotal(IQueryable query, DataTable dt)
        {
            //Подсчет итогов
            var totals = new Dictionary<string, decimal>();
            foreach (var c in VisibleColumns.Where(col => col.CountTotal))
            {
                PropertyInfo prop = null;
                decimal total = 0;
                int i = 0;
                foreach (var e in query)
                {
                    if (i == 0 && prop == null) prop = e.GetType().GetProperty(c.Id);
                    //Значение для столбца "Итого" может браться из свойства объекта, если это свойство присутствует у объекта, или из ячейки таблицы.
                    //Если prop != null - берем из свойства объекта, иначе из ячейки таблицы.
                    var val = prop != null ? prop.GetValue(e, null) : dt.Rows[i][c.Id];
                    if (val != null && val != DBNull.Value) total += Convert.ToDecimal(val);
                    i++;
                }
                totals[c.Id] = total;
            }
            //Результаты подсчета итогов заносятся во ViewData в массив "Totals"
            if (totals.Any(t => t.Value > 0))
                ViewData["Total"] = totals;
        }

        /// <summary>
        /// Дочерняя таблица может перегрузить эту функцию чтобы внести изменения в сформированную таблицу перед выдачей
        /// </summary>
        /// <param name="dt">DataTable</param>
        protected virtual void PrepareDataTable(TDataContext dc, ref DataTable dt) {}

        /// <summary>
        /// Функция вызывается после добавления нового ряда в таблицу. Дочерняя таблица может перегрузить эту функцию. 
        /// </summary>
        /// <param name="entity">TEntity</param>
        /// <param name="dt">DataTable</param>
        protected virtual void RowAdded(TDataContext dc, object entity, ref DataRow dr) {}

        /// <summary>
        /// Получить форматы ячеек для ряда с данными
        /// </summary>
        /// <param name="entity">Текущий объект</param>
        /// <returns>Коллекция форматов</returns>
        protected virtual NameValueCollection FormatRow(object entity)
        {
            var format = new NameValueCollection();
            foreach (var col in VisibleColumns)
            {
                if (ViewData.ContainsKey("excel"))
                    format[col.Id] = col.ExcelFormat ?? "{@" + col.Id + "}";
                else
                    format[col.Id] = col.Format ?? "{@" + col.Id + "}";
            }
            foreach (var col in SystemColumns)
            {
                if (ViewData.ContainsKey("excel"))
                    format[col.Id] = col.ExcelFormat ?? "{@" + col.Id + "}";
                else
                    format[col.Id] = col.Format ?? "{@" + col.Id + "}";
            }

            //Если первичного ключа нет среди колонок добавим формат для первичного ключа принудительно
            if (!string.IsNullOrEmpty(_PrimaryKey) && !VisibleColumns.Any(vc => vc.Id == _PrimaryKey))
                format[_PrimaryKey] = "{@" + _PrimaryKey + "}";

            //Зададим формат для колноки "RowClass"
            if (_HasRowClass) format["RowClass"] = "{@RowClass}";

            return format;
        }

        #endregion

        #region 4. Сохранение состояний
        /// <summary>
        /// Метод для загрузки карточки фильтра
        /// </summary>
        public virtual void LoadFilter()
        {
            using (var dc = new TDataContext())
            {
                if (string.IsNullOrEmpty(TemplatePath)) TemplatePath = _FilterTemplatePath;
                LayoutPath = "none";

                InitConfigurator();
                InitState();
                CustomInit(dc);

                //CreateColumns(dc);
                CreateFilterConditions(dc);

                //InitColumns();
                InitFilter();

                RenderView();
            }
        }

        /// <summary>
        /// Метод для загрузки карточки списка колонок
        /// </summary>
        public virtual void LoadColumns()
        {
            using (var dc = new TDataContext())
            {
                if (string.IsNullOrEmpty(TemplatePath)) TemplatePath = _ColumnsTemplatePath;
                LayoutPath = "none";

                InitConfigurator();
                InitState();
                CustomInit(dc);

                CreateColumns(dc);

                InitColumns();
                InitPaging();

                RenderView();
            }
        }

        /// <summary>
        /// Сохраняет текущее состояние таблицы в БД в таблицу стандартной структуры
        /// </summary>
        public virtual void SaveState()
        {
            InitConfigurator();
            if (!_SaveFilter) return;

            InitState();

            try
            {
                //Пользователь нужен, чтобы сохранить персональное представление
                if (!ViewData.ContainsKey("user") || ViewData["user"] == null) throw new ArgumentNullException("Авторизуйтесь, чтобы иметь возможность сохранять фильтры.");
                var user = (ViewData["user"] as IUser);
                
                using (var dc = new TDataContext())
                {
                    if (dc.Connection.State != ConnectionState.Open)
                        dc.Connection.Open();

                    int oldFilterId = 0;
                    string filterName = Request["filtername"];
                    string state = SerializeState();

                    if (string.IsNullOrEmpty(filterName)) throw new ArgumentException("Поле \"Название фильтра\" не заполнено.");
                    if (!string.IsNullOrEmpty(Request["oldfilterid"])) Int32.TryParse(Request["oldfilterid"], out oldFilterId);
                    if (string.IsNullOrEmpty(state)) throw new ArgumentException("Невозможно сохранить пустой фильтр");

                    var cmd = new SqlCommand("", (SqlConnection)dc.Connection);
                    cmd.Parameters.AddWithValue("@Page", Name);
                    if (string.IsNullOrEmpty(Request["commonfilter"]))
                    {
                        cmd.Parameters.AddWithValue("@IsDefault", !string.IsNullOrEmpty(Request["isdefault"]));
                        cmd.Parameters.AddWithValue("@UserId", user.Id);
                    }
                    else
                    {
                        cmd.Parameters.AddWithValue("@IsDefault", !string.IsNullOrEmpty(Request["isdefault"]));
                        cmd.Parameters.AddWithValue("@UserId", DBNull.Value);
                    }

                    cmd.Parameters.AddWithValue("@Name", filterName);
                    cmd.Parameters.AddWithValue("@Filter", state);

                    if (oldFilterId == 0)
                        cmd.CommandText = "INSERT INTO Filter (Name, UserId, Page, Filter, IsDefault) VALUES(@Name,@UserId,@Page,@Filter,@IsDefault) SELECT SCOPE_IDENTITY()";
                    else
                    {
                        cmd.CommandText = "UPDATE Filter SET UserId=@UserId,Name=@Name,Filter=@Filter,IsDefault=@IsDefault WHERE Id=@Id AND Page=@Page SELECT @Id";
                        cmd.Parameters.AddWithValue("@Id", oldFilterId);
                    }

                    int filterId = Convert.ToInt32(cmd.ExecuteScalar(), CultureInfo.InvariantCulture);
                }
            }
            catch (Exception ex)
            {
                ViewData["SaveStateException"] = ex.Message;
            }

            LoadSavedStates();
        }

        /// <summary>
        /// Удалить состояние таблицы из БД
        /// </summary>
        /// <param name="id"></param>
        public virtual void DeleteState(string id)
        {
            InitConfigurator();
            if (!_SaveFilter) return;

            try
            {
                int filterid = 0;
                if (!Int32.TryParse(id, out filterid))
                    throw new ArgumentException("Неправильный идентификатор фильтра.");

                using (var dc = new TDataContext())
                {
                    dc.Connection.Open();
                    var cmd = new SqlCommand("DELETE FROM Filter WHERE Id = @Id AND Page = @Page", (dc.Connection as SqlConnection));
                    cmd.Parameters.AddWithValue("@Id", filterid);
                    cmd.Parameters.AddWithValue("@Page", Name);

                    cmd.ExecuteNonQuery();
                }
            }
            catch (SqlException ex)
            {
                ViewData["SaveStateException"] = ex.Message;
            }

            LoadSavedStates();
        }

        /// <summary>
        /// Загружает список сохраненных представлений
        /// </summary>
        public virtual void LoadSavedStates()
        {
            InitConfigurator();
            if (!_SaveFilter) return;

            LayoutPath = "none";
            TemplatePath = _SavedStatesTemplatePath;

            InitState(); //Заново, чтобы загрузить представление, которое, возможно, было добавлено
            RenderView();
        }

        /// <summary>
        /// Загружает форму сохранения представлений
        /// </summary>
        public virtual void ShowSaveState()
        {
            InitConfigurator();
            if (!_SaveFilter) return;

            LayoutPath = "none";
            TemplatePath = "~/views/_table/savefilter.haml";

            InitState();
            RenderView();
        }

        /// <summary>
        /// Загрузить состояние таблицы по идентификатору.
        /// </summary>
        /// <param name="stateid">0, если нужно состояние по умолчанию</param>
        protected virtual void LoadState(int stateid)
        {
            //Пользователь нужен, чтобы проверить права на загружаемое представление
            IUser user = null;
            if (ViewData.ContainsKey("user") && ViewData["user"] != null) 
                user = ViewData["user"] as IUser;

            using (var dc = new TDataContext())
            {
                if (dc.Connection.State != ConnectionState.Open)
                    dc.Connection.Open();

                var cmd = new SqlCommand("", (SqlConnection)dc.Connection);
                cmd.Parameters.AddWithValue("@Page", Name);
                cmd.Parameters.AddWithValue("@UserId", user != null ? user.Id : 0);
                cmd.Parameters.AddWithValue("@Id", stateid);

                cmd.CommandText = "SELECT TOP 1 Id,Filter FROM Filter WHERE (@Id=0 OR @Id=Id) AND (ISNULL(UserId,@UserId)=@UserId) AND Page=@Page AND (@Id>0 OR IsDefault=1) ORDER BY UserId DESC";
                using (var dr = cmd.ExecuteReader())
                {
                    if (dr.HasRows)
                    {
                        string stateKey = Name + ViewData["ActionKey"];

                        //Сохраним старое состояние
                        var oldstate = (Session[stateKey] as NameValueCollection) != null ? new NameValueCollection(Session[stateKey] as NameValueCollection) : new NameValueCollection();

                        dr.Read();

                        //Считаем состояние из базы
                        State = DeserializeState(dr.GetString(1));

                        //Восстановим те ключи, которые не должны быть сброшены
                        foreach (var key in _FilterIndependentKeys)
                            State[key] = oldstate[key];

                        State["tb_stateid"] = dr.GetInt32(0).ToString();
                    }
                }
            }
        }
        #endregion

        #region Служебные методы
        /// <summary>
        /// Сохранить текущее состояние в строку вида "tb_columns=Id,Name&tb_orderby=Id DESC..."
        /// </summary>
        /// <returns>String</returns>
        protected string SerializeState()
        {
            if (State == null) return "";
            var sb = new StringBuilder();
            foreach (string key in State.AllKeys.Where(k => !_ExcludeStateKeys.Contains(k)))
                sb.AppendFormat(CultureInfo.InvariantCulture, "{0}={1}&", key, Convert.ToString(State[key]).Replace("&","%26"));
            return sb.ToString();
        }

        /// <summary>
        /// Получить состояние из строки вида "tb_columns=Id,Name&tb_orderby=Id DESC..."
        /// </summary>
        /// <param name="filter"></param>
        /// <returns></returns>
        protected NameValueCollection DeserializeState(string filter)
        {
            var result = new NameValueCollection();
            string[] tmps = filter.Split('&');
            foreach (string tmp in tmps)
            {
                if (string.IsNullOrEmpty(tmp) || tmp.IndexOf('=') < 0 || tmp.IndexOf('=') == tmp.Length - 1) continue;
                result[tmp.Substring(0, tmp.IndexOf('='))] = tmp.Substring(tmp.IndexOf('=') + 1).Replace("%26", "&");
            }
            return result;
        }

        /// <summary>
        /// Показывает, применен ли фильтр, путем сравнения текущего состояния и состояния по умолчанию.
        /// </summary>
        /// <param name="state">Текущее состояние</param>
        /// <returns>true/false</returns>
        private bool FilterApplied(NameValueCollection state)
        {
            var statekeys = String.Join(",", (from k in State.AllKeys where k.ToUpperInvariant().StartsWith("FLT") || k.ToUpperInvariant().StartsWith("FTT") orderby k select state[k]).ToArray());
            var defaultkeys = String.Join(",", (from k in _DefaultState.AllKeys where k.ToUpperInvariant().StartsWith("FLT") || k.ToUpperInvariant().StartsWith("FTT") orderby k select state[k]).ToArray());
            return statekeys != defaultkeys;
        }

        /// <summary>
        /// Показывает, изменился ли состав колонок, путем сравнения текущего состояния и состояния по умолчанию.
        /// </summary>
        /// <param name="state">Текущее состояние</param>
        /// <returns>true/false</returns>
        private bool ColumnsChanged(NameValueCollection state)
        {
            return (State["tb_columns"] != _DefaultState["tb_columns"] || State["tb_orderby"] != _DefaultState["tb_orderby"]);
        }

        /// <summary>
        /// Дополнительно зарегистрируем скрипт для drag-drop'а колонок.
        /// </summary>
        public override void RegisterTableScripts()
        {
            base.RegisterTableScripts();

            RegisterScript("jquery.ui.packed.js");
        }
        #endregion
    }
}
